// Copyright 2025 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * This file is mostly generated by Gemini.
 *
 * The intention is that this will be used to generate fake data for a TurboCI graph while the
 * corresponding Turbo CI QueryNodes API meant to provide this data is still in development. Once
 * the API is available and integrated, we can remove this file.
 */

import { Duration } from '../proto/google/protobuf/duration.pb';
import {
  Identifier,
  WorkPlan,
  Check as CheckId,
  Stage as StageId,
  CheckOption as CheckOptionId,
  CheckResult as CheckResultId,
  CheckResultDatum as CheckResultDatumId,
} from '../proto/turboci/graph/ids/v1/identifier.pb';
import { Actor } from '../proto/turboci/graph/orchestrator/v1/actor.pb';
import {
  Check,
  Check_OptionRef,
  Check_Result,
} from '../proto/turboci/graph/orchestrator/v1/check.pb';
import { CheckDelta } from '../proto/turboci/graph/orchestrator/v1/check_delta.pb';
import { CheckEditView } from '../proto/turboci/graph/orchestrator/v1/check_edit_view.pb';
import { CheckKind } from '../proto/turboci/graph/orchestrator/v1/check_kind.pb';
import { CheckResultView } from '../proto/turboci/graph/orchestrator/v1/check_result_view.pb';
import {
  CheckState,
  checkStateToJSON,
} from '../proto/turboci/graph/orchestrator/v1/check_state.pb';
import { CheckView } from '../proto/turboci/graph/orchestrator/v1/check_view.pb';
import { Datum } from '../proto/turboci/graph/orchestrator/v1/datum.pb';
import {
  Edge,
  Edge_Resolution,
} from '../proto/turboci/graph/orchestrator/v1/edge.pb';
import { EdgeGroup } from '../proto/turboci/graph/orchestrator/v1/edge_group.pb';
import { Edit } from '../proto/turboci/graph/orchestrator/v1/edit.pb';
import {
  ExecutionPolicy,
  ExecutionPolicy_StageTimeoutMode,
} from '../proto/turboci/graph/orchestrator/v1/execution_policy.pb';
import { GraphView } from '../proto/turboci/graph/orchestrator/v1/graph_view.pb';
import { Revision } from '../proto/turboci/graph/orchestrator/v1/revision.pb';
import {
  Stage,
  Stage_Assignment,
  Stage_Attempt,
  Stage_ExecutionPolicyState,
} from '../proto/turboci/graph/orchestrator/v1/stage.pb';
import { StageAttemptState } from '../proto/turboci/graph/orchestrator/v1/stage_attempt_state.pb';
import { StageDelta } from '../proto/turboci/graph/orchestrator/v1/stage_delta.pb';
import { StageEditView } from '../proto/turboci/graph/orchestrator/v1/stage_edit_view.pb';
import { StageState } from '../proto/turboci/graph/orchestrator/v1/stage_state.pb';
import { StageView } from '../proto/turboci/graph/orchestrator/v1/stage_view.pb';
import { Value } from '../proto/turboci/graph/orchestrator/v1/value.pb';

/**
 * Configuration for the FakeGraphGenerator.
 */
export interface GraphGenerationConfig {
  workPlanIdStr: string;
  /** List of unique string IDs for Checks */
  checkIds: string[];
  /** List of unique string IDs for Stages */
  stageIds: string[];
  /**
   * Adjacency list for dependencies.
   * Key is the ID of the node (Check or Stage) that has dependencies.
   * Value is a list of IDs (Checks or Stages) that it depends on.
   */
  dependencies: Record<string, string[]>;
  /**
   * Optional mapping of Check ID to its CheckKind.
   * If not provided, defaults to CHECK_KIND_BUILD.
   */
  checkKinds?: Record<string, CheckKind>;
  /**
   * Optional list of check edits.
   * Each edit represents a stage editing a check's state.
   */
  checkEdits?: { stageId: string; checkId: string; state: CheckState }[];
}

export class FakeGraphGenerator {
  private workPlanId: WorkPlan;
  private checkIdMap: Map<string, CheckId> = new Map();
  private stageIdMap: Map<string, StageId> = new Map();
  private genericIdMap: Map<string, Identifier> = new Map();

  // Simulated time to ensure Revisions allow logical ordering (createdAt < finalizedAt)
  private currentSimulatedTimeMs = Date.UTC(2024, 0, 1, 12, 0, 0);

  constructor(private config: GraphGenerationConfig) {
    // 1. Initialize base Identifiers
    this.workPlanId = { id: config.workPlanIdStr };

    // 2. Create all Node Identifiers first so dependencies can be resolved.
    for (const id of config.checkIds) {
      const checkId: CheckId = { workPlan: this.workPlanId, id: id };
      this.checkIdMap.set(id, checkId);
      this.genericIdMap.set(id, { check: checkId });
    }

    for (const id of config.stageIds) {
      // Ensure ID format complies with proto comments (S or N prefix)
      const formattedId = id.match(/^[SN]/) ? id : `S_${id}`;
      const stageId: StageId = { workPlan: this.workPlanId, id: formattedId };
      this.stageIdMap.set(id, stageId);
      this.genericIdMap.set(id, { stage: stageId });
    }
  }

  /**
   * Generates the complete GraphView based on the configuration.
   */
  public generate(): GraphView {
    const checkViews: CheckView[] = this.config.checkIds.map((id) =>
      this.generateCheckView(id),
    );
    const stageViews: StageView[] = this.config.stageIds.map((id) =>
      this.generateStageView(id),
    );

    return {
      version: this.nextRevision(),
      checks: checkViews,
      stages: stageViews,
    };
  }

  // ==========================================
  // Core View Generators
  // ==========================================

  private generateCheckView(idStr: string): CheckView {
    const checkId = this.checkIdMap.get(idStr)!;
    const realm = `${idStr}-realm`; // Deterministic realm

    // Generate foundational Check structure
    const check: Check = {
      identifier: checkId,
      // Use the configured kind, or default to BUILD.
      kind: this.config.checkKinds?.[idStr] ?? CheckKind.CHECK_KIND_BUILD,
      realm: realm,
      version: this.nextRevision(),
      state: CheckState.CHECK_STATE_WAITING,
      dependencies: this.resolveDependencies(idStr),
      options: [], // Filled below
      results: [], // Filled below
    };

    const optionData: Datum[] = [];
    const resultsViews: CheckResultView[] = [];

    // Populate 2 hardcoded Options
    for (let i = 1; i <= 2; i++) {
      const optId: CheckOptionId = { check: checkId, idx: i };
      const typeUrl = `type.googleapis.com/turboci.demo.CheckOptionConfig`;

      // Add ref to Check
      (check.options as Check_OptionRef[]).push({
        identifier: optId,
        typeUrl: typeUrl,
      });

      // Create actual Datum
      optionData.push(
        this.createDatum(
          { checkOption: optId },
          realm,
          typeUrl,
          `Option ${i} for ${idStr}`,
        ),
      );
    }

    // Populate 1 hardcoded Result with 1 Datum
    const resultId: CheckResultId = { check: checkId, idx: 1 };
    const datumId: CheckResultDatumId = { result: resultId, idx: 1 };
    const resTypeUrl = `type.googleapis.com/turboci.demo.BuildArtifact`;

    const checkResult: Check_Result = {
      identifier: resultId,
      owner: this.getOrchestratorActor(),
      createdAt: this.nextRevision(),
      data: [{ identifier: datumId, typeUrl: resTypeUrl }],
      // Result not finalized yet in this "WAITING" state example
      finalizedAt: undefined,
    };
    (check.results as Check_Result[]).push(checkResult);

    // The view of the data for that result
    resultsViews.push({
      data: [
        this.createDatum(
          { checkResultDatum: datumId },
          realm,
          resTypeUrl,
          `Artifact for ${idStr}`,
        ),
      ],
    });

    // Generate edits for this check
    const edits: CheckEditView[] = [];
    const checkEdits = this.config.checkEdits?.filter(
      (e) => e.checkId === idStr,
    );

    if (checkEdits) {
      for (const editInfo of checkEdits) {
        const stageId = this.stageIdMap.get(editInfo.stageId);
        if (!stageId) {
          throw new Error(
            `Stage ID ${editInfo.stageId} for check edit not found in configured stage IDs.`,
          );
        }

        const editVersion = this.nextRevision();

        const checkDelta: CheckDelta = {
          state: editInfo.state,
          dependencies: [],
          options: [],
          result: [],
        };

        const editor: Actor = {
          stageAttempt: {
            stage: stageId,
          },
        };

        const edit: Edit = this.createEdit(
          { check: checkId },
          realm,
          editVersion,
          `State changed to ${checkStateToJSON(editInfo.state)}`,
          { check: checkDelta },
          editor,
        );

        edits.push({ edit: edit, optionData: [] });
      }
    }

    return {
      check: check,
      optionData: optionData,
      edits: edits,
      results: resultsViews,
    };
  }

  private generateStageView(idStr: string): StageView {
    const stageId = this.stageIdMap.get(idStr)!;
    const realm = `${idStr}-realm`;

    const stage: Stage = {
      identifier: stageId,
      realm: realm,
      createTs: this.nextRevision(),
      args: this.createValue(
        'type.googleapis.com/turboci.demo.StageArgs',
        `Args for ${idStr}`,
      ),
      version: this.nextRevision(),
      // Hardcoded "running" state
      state: StageState.STAGE_STATE_ATTEMPTING,
      dependencies: this.resolveDependencies(idStr),
      executionPolicy: this.createExecutionPolicyState(),
      attempts: this.generateActiveStageAttempt(idStr),
      assignments: this.generateSampleAssignment(),
      continuationGroup: [], // Kept empty for simplicity
    };

    const edits: StageEditView[] = this.generateSampleStageEdit(stageId, realm);

    return {
      stage: stage,
      edits: edits,
    };
  }

  // ==========================================
  // Dependency / Edge Logic
  // ==========================================

  private resolveDependencies(sourceIdStr: string): EdgeGroup[] {
    const targetIds = this.config.dependencies[sourceIdStr];
    if (!targetIds || targetIds.length === 0) {
      return [];
    }

    // Create a simple AND group of all dependencies, all resolved.
    const edges: Edge[] = targetIds.map((targetIdStr) => {
      const targetIdentifier = this.genericIdMap.get(targetIdStr);
      if (!targetIdentifier) {
        throw new Error(
          `Dependency target ID specified but not found in configured IDs: ${targetIdStr}`,
        );
      }
      return this.createResolvedEdge(targetIdentifier);
    });

    return [
      {
        edges: edges,
        groups: [],
        threshold: undefined, // Implies AND all edges
        resolution: this.createResolution(), // Group is resolved
      },
    ];
  }

  private createResolvedEdge(target: Identifier): Edge {
    return {
      target: target,
      resolution: this.createResolution(),
    };
  }

  private createResolution(): Edge_Resolution {
    const targetV = this.nextRevision();
    return {
      satisfied: true,
      targetVersion: targetV,
      at: this.nextRevision(), // 'at' must be >= targetVersion
    };
  }

  // ==========================================
  // Complex Sub-Object Generators
  // ==========================================

  private generateActiveStageAttempt(stageIdStr: string): Stage_Attempt[] {
    // One historical failed attempt, one currently running attempt.
    const histRev = this.nextRevision();
    const historical: Stage_Attempt = {
      state: StageAttemptState.STAGE_ATTEMPT_STATE_INCOMPLETE,
      version: histRev,
      details: [
        this.createValue('type.u/log', `Attempt 1 failed for ${stageIdStr}`),
      ],
      progress: [
        { msg: 'Setup done', version: histRev, details: [] },
        { msg: 'Failed', version: histRev, details: [] },
      ],
    };

    const activeRev = this.nextRevision();
    const active: Stage_Attempt = {
      state: StageAttemptState.STAGE_ATTEMPT_STATE_RUNNING,
      version: activeRev,
      processUid: `worker-pool-REGION-1:${stageIdStr}:attempt-2`,
      details: [
        this.createValue(
          'type.u/executor-link',
          `http://executor.example.com/task/${stageIdStr}`,
        ),
      ],
      progress: [{ msg: 'Initialized', version: activeRev, details: [] }],
    };

    return [historical, active];
  }

  private generateSampleAssignment(): Stage_Assignment[] {
    // If there are checks, assign this stage to the first one as a sample.
    if (this.config.checkIds.length > 0) {
      return [
        {
          target: this.checkIdMap.get(this.config.checkIds[0]),
          goalState: CheckState.CHECK_STATE_PLANNED,
        },
      ];
    }
    return [];
  }

  private createExecutionPolicyState(): Stage_ExecutionPolicyState {
    const policy = this.createExecutionPolicy();
    return {
      requested: policy,
      validated: policy, // Assume accepted as-is
    };
  }

  private createExecutionPolicy(): ExecutionPolicy {
    return {
      // Standard hardcoded timeouts
      attemptHeartbeat: { running: this.createDuration(60) },
      attemptTimeout: { running: this.createDuration(3600) },
      retry: { maxRetries: 3 },
      stageTimeout: this.createDuration(7200),
      stageTimeoutMode:
        ExecutionPolicy_StageTimeoutMode.STAGE_TIMEOUT_MODE_FINISH_CURRENT_ATTEMPT,
    };
  }

  // ==========================================
  // Edit and Delta Generators
  // ==========================================

  private generateSampleStageEdit(
    stageId: StageId,
    realm: string,
  ): StageEditView[] {
    const editVersion = this.nextRevision();

    // An edit that updated policy
    const delta: StageDelta = {
      executionPolicies: [this.createExecutionPolicy()],
    };

    const edit: Edit = this.createEdit(
      { stage: stageId },
      realm,
      editVersion,
      'Executor validated policies',
      { stage: delta },
    );

    return [{ edit: edit }];
  }

  private createEdit(
    forNode: Identifier,
    realm: string,
    version: Revision,
    reasonMsg: string,
    delta: { check?: CheckDelta; stage?: StageDelta },
    editor?: Actor,
  ): Edit {
    // Hardcoded expiries roughly 30/180 days in future based on start time
    const futureTime = this.currentSimulatedTimeMs + 30 * 24 * 60 * 60 * 1000;
    const expireStr = new Date(futureTime).toISOString();

    return {
      forNode: forNode,
      version: version,
      expireAt: expireStr, // Roughly +180 days
      dataExpireAt: expireStr, // Roughly +30 days
      realm: realm,
      editor: editor || this.getOrchestratorActor(),
      transactionalSet: [forNode],
      reasons: [
        {
          realm: realm,
          reason: reasonMsg,
          details: [],
        },
      ],
      check: delta.check,
      stage: delta.stage,
    };
  }

  // ==========================================
  // Primitive & Common Type Helpers
  // ==========================================

  /** Advances internal timer and returns a new Revision string */
  private nextRevision(): Revision {
    this.currentSimulatedTimeMs += 1000; // Advance 1 second per call
    // Format: T<seconds>/<nanos> or ISO string depending on impl.
    // Using ISO for standard readability.
    return { ts: new Date(this.currentSimulatedTimeMs).toISOString() };
  }

  private createDuration(seconds: number): Duration {
    return {
      seconds: seconds.toString(),
      nanos: 0,
    };
  }

  private getOrchestratorActor(): Actor {
    return { orchestrator: {} };
  }

  private createDatum(
    id: Identifier,
    realm: string,
    typeUrl: string,
    description: string,
  ): Datum {
    return {
      identifier: id,
      realm: realm,
      version: this.nextRevision(),
      value: this.createValue(typeUrl, description),
    };
  }

  private createValue(typeUrl: string, description: string): Value {
    // Create a hardcoded JSON payload representing the Any
    const jsonContent = JSON.stringify({
      '@type': typeUrl,
      description: description,
      static_flag: true,
    });

    return {
      // Binary 'value' is omitted, relying on valueJson for view representation
      hasUnknownFields: false,
      valueJson: jsonContent,
    };
  }
}
